package smallChanges.lineNumberMapping;

import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import smallChanges.statements.Statement;
import smallChanges.statements.StatementMappingEntry;
import smallChanges.statements.StatementMappingResultSet;
import smallChanges.statements.StatementMappingEntry.StatementMappingStatus;

/**
 * Mapping of lines.  One line of the LHS may map to multiple
 * lines on the RHS (hence the Map&lt;Integer,Set&lt;Integer&gt;&gt;).
 *   
 * 
 * @author jspacco
 *
 */
public class LineMapping
{

    
    private Map<Integer, Set<Integer>> changed = new TreeMap<Integer, Set<Integer>>();
    /**
     * Note that the "correct" results used by ReissConfig for the
     * reference solutions don't use unchanged at all.  Only LDiff
     * uses unchanged.
     */
    private Map<Integer, Integer> unchanged = new TreeMap<Integer, Integer>();
    /**
     * @deprecated
     */
    private Set<Integer> deleted = new TreeSet<Integer>();
    
    public boolean matches(int lhs, int rhs) {
        if (unchanged.containsKey(lhs) && unchanged.get(lhs)==rhs) {
            // If the lines are unchanged, then they match
            return true;
        }
        if (changed.containsKey(lhs)) {
            // If the lines were changed, and the lhs maps to
            // any line on the RHS, then they match
            Set<Integer> set=changed.get(lhs);
            return set.contains(rhs);
        }
        // they don't match
        return false;
    }
    
    /**
     * 
     * Create mapping entries from the LHS statement 
     * to the RHS statement
     * If we have something like this:
     * 10:    f(x,
     * 11:      y,
     * 12:      z)
     * 
     * changing to this:
     * 
     * 10:    f(x,
     * 11:      y,
     * 12:      z,
     * 13:      w)
     *
     * We'll map it like this:
     * 
     * 10 => 10
     * 11 => 11
     * 12 => [12, 13]
     * 
     * That way statement mapping will score 4/4 while any line-based algorithm will score 
     * at most 3/4.
     * 
     * If we map in the opposite direction (i.e. form 4 lines down to 3)
     * we'll track it as:
     * 10 => 10
     * 11 => 11
     * 12 => 12
     * and ignore the last line.  Statement mapping will score 3/3 
     * and line-based algorithms will see a false-positive delete.
     * 
     * @param rs The mapping result set produced by chadDiff.
     */
    public LineMapping(StatementMappingResultSet rs, 
            boolean useProbable,
            boolean usePossible)
    {
        for (StatementMappingEntry entry : rs) {
            if (entry.getStatus().equals(StatementMappingStatus.INVALID)) {
                continue;
            }
            if (entry.getStatus().equals(StatementMappingStatus.POSSIBLE) &&
                    !usePossible)
            {
                continue;
            }
            if (entry.getStatus().equals(StatementMappingStatus.PROBABLE) &&
                    !useProbable)
            {
                continue;
            }

            Statement lhsStatement=entry.getLeftStatement();
            Statement rhsStatement=entry.getRightStatement();
            
            int lhsSize=lhsStatement.getEndLine()-lhsStatement.getBeginLine()+1;
            int rhsSize=rhsStatement.getEndLine()-rhsStatement.getBeginLine()+1;
            if (rhsSize==-1) {
                // The LHS statement was deleted
                for (int lhs=lhsStatement.getBeginLine(); 
                    lhs<=lhsStatement.getEndLine(); 
                    lhs++)
                {
                    addDeleted(lhs);
                }
            } else if (lhsSize==rhsSize) {
                // match lines 1-to-1
                int rhs=rhsStatement.getBeginLine();
                for (int lhs=lhsStatement.getBeginLine(); 
                    lhs<=lhsStatement.getEndLine();
                    lhs++)
                {
                    // TODO: Handle token edit distance and cosine distances
                    if (entry.getNormalizedEditDistance()>0.0) {
                        addChanged(lhs, rhs);
                    } else {
                        addUnchanged(lhs, rhs);
                    }
                    rhs++;
                }
                
            } else if (lhsSize < rhsSize) {
                // Map all but the last LHS lines to their corresponding RHS lines
                int rhs=rhsStatement.getBeginLine();
                for (int lhs=lhsStatement.getBeginLine(); 
                    lhs < lhsStatement.getEndLine();
                    lhs++)
                {
                    // since LHS and RHS statements have different sizes
                    // this is by definition a change
                    addChanged(lhs, rhs);
                    rhs++;
                }
                // Map the last LHS line to the remaining RHS lines
                for (; rhs<=rhsStatement.getEndLine(); rhs++) {
                    addChanged(lhsStatement.getEndLine(), rhs);
                }
            } else {
                // lhsSize > rhsSize
                // map the first rhsSize number of lines from
                // LHS => RHS
                // XXX we're ignoring the remaining lines in the RHS
                // This is (hopefully) OK because line-based algorithms
                // will think there have been deletes, when in actuality
                // there haven't been any deletes, so they'll have
                // false-positive.  ChadDiff will get credit for three hits
                // here and won't have any false-positives.
                // This is kind of a weird accounting system
                // but that's what we've got
                int lhs=lhsStatement.getBeginLine();
                for (int rhs=rhsStatement.getBeginLine(); rhs<=rhsStatement.getEndLine(); rhs++) {
                    addChanged(lhs, rhs);
                    lhs++;
                }
            }
        }
    }
    
    public boolean isDelete(int lhs) {
        return deleted.contains(lhs);
    }

    public LineMapping() {
        super();
    }
    
    public void remove(int lhs, int rhs) {
        if (rhs==-1) {
            deleted.remove(lhs);
        }
        if (changed.containsKey(lhs)) {
            Set<Integer> set=changed.get(lhs);
            if (set.size()==1) {
                changed.remove(lhs);
            }
            set.remove(rhs);
        }
    }

    public LineMapping(LineMapping mapping) {
        // copy constructor
        this();
        for (Map.Entry<Integer,Set<Integer>> entry : mapping.getChanged().entrySet()) {
            int lhs=entry.getKey();
            for (int rhs : entry.getValue()) {
                if (rhs==-1) {
                    addDeleted(rhs);
                }
                addChanged(lhs,rhs);
            }
        }
    }

    public Map<Integer,Integer> getUnchanged()
    {
        return unchanged;
    }

    public Map<Integer,Set<Integer>> getChanged()
    {
        return changed;
    }

    public Set<Integer> getDeleted()
    {
        return deleted;
    }
    
    public void addChanged(int lhs, int rhs) {
        if (!changed.containsKey(lhs)) {
            changed.put(lhs, new TreeSet<Integer>());
        }
        changed.get(lhs).add(rhs);
    }
    
    public void addUnchanged(int lhs, int rhs) {
        unchanged.put(lhs, rhs);
    }
    
    public void addDeleted(int lhs) {
        deleted.add(lhs);
    }

    public String toString()
    {
        StringBuffer buf=new StringBuffer();
        buf.append("unchanged:\n");
        for (Map.Entry<Integer, Integer> entry : unchanged.entrySet()) {
            buf.append("\t"+entry.getKey()+" => "+entry.getValue()+"\n");
        }
        buf.append("changed:\n");
        for (Map.Entry<Integer, Set<Integer>> entry : changed.entrySet()) {
            for (Integer i : entry.getValue()) {
                buf.append("\t"+entry.getKey()+" => "+i+"\n");
            }
        }
        buf.append("deleted:\n");
        for (Integer i : deleted) {
            buf.append("\t"+i+"\n");
        }
        return buf.toString();
    }

    public int findMatch(int lhs, int rhs)
    {
        if (unchanged.containsKey(lhs)) {
            // This line was mapped as unchanged
            return unchanged.get(lhs);
        }
        if (changed.containsKey(lhs)) {
            // The line was mapped as changed
            // If it mapped to many lines and this was one of them
            // then return it.
            // Otherwise, just return the first line in the set
            // since it will be wrong at any rate
            Set<Integer> set=changed.get(lhs);
            if (set.contains(rhs))
                return rhs;
            return (Integer)changed.get(lhs).toArray()[0];
        }
        // can't find this line anywhere...
        // we think it's a delete
        return -1;
    }

}